// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace SnapObjects.Data
{
    internal abstract class UpdateCommand
    {
        protected readonly DataContext _dataContext;
        protected readonly IAdoDbProvider _adoDbProvider;
        protected readonly ITypeMapper _typeMapper;

        public UpdateCommand(DataContext dataContext)
        {
            _dataContext = dataContext;
            _adoDbProvider = dataContext.AdoDbProviderFactory.GetAdoDbProvider();
            _typeMapper = dataContext.AdoDbSqlProvider.TypeMapper;
        }

        public int InsertedCount { get; protected set; }

        public int DeletedCount { get; protected set; }

        public int ModifiedCount { get; protected set; }

        //public int SqlCUDCount { get; protected set; }

        public string BuiltSql { get; protected set; }

        public virtual void InitCommand()
        {
            this.InsertedCount = 0;
            this.DeletedCount = 0;
            this.ModifiedCount = 0;
            //SqlCUDCount = 0;
            this.BuiltSql = String.Empty;
        }


        protected void ConsumeResult(
            ModelUpdateInfo modelUpdateInfo,
            ResultSetMapping resultSetMapping,
            IAdoDbDataVisitor dataVisitor)
        {
            if (modelUpdateInfo.ModelState == ModelState.NewModified)
            {
                this.InsertAffect(modelUpdateInfo, resultSetMapping, dataVisitor);
            }
            else if (modelUpdateInfo.ModelState == ModelState.Modified ||
                modelUpdateInfo.ModelState == ModelState.Deleted)
            {
                this.ModifyOrDeleteAffect(modelUpdateInfo, resultSetMapping, dataVisitor);
            }
        }

        protected void ConsumeResult(
            IUpdatableSql updatableSql,
            ResultSetMapping resultSetMapping,
            IAdoDbDataVisitor dataVisitor)
        {
            var hasResult = true;

            if (!dataVisitor.MoveNext())
            {
                hasResult = false;
            }

            var value = 0;

            if (hasResult)
            {
                var dataReader = dataVisitor.ReadData();

                // sqlite return long type, so change to "convert" function
                value = Convert.ToInt32(this.GetDbValue(dataReader, 0));
            }
            else
            {
                var outputParameters = dataVisitor.GetOutputParameters();

                if (outputParameters.Count > 0)
                {
                    // sqlite return long type, so change to "convert" function
                    value = Convert.ToInt32(outputParameters.FirstOrDefault().Value);
                }
            }

            if (value > 0)
            {
                this.UpdateAffectCount(updatableSql, value);
            }
        }

        private void UpdateAffectCount(IUpdatableSql updatableSql, int count)
        {
            var sql = updatableSql.SqlText.TrimStart();

            if (sql.StartsWith("Insert", StringComparison.OrdinalIgnoreCase))
            {
                this.InsertedCount += count;
            }
            else if (sql.StartsWith("Delete", StringComparison.OrdinalIgnoreCase))
            {
                this.DeletedCount += count;
            }
            else if (sql.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
            {
                this.ModifiedCount += count;
            }
        }

        private object GetDbValue(IDataReader dataReader, int index)
        {
            var value = dataReader.GetValue(index);
            var dataType = dataReader.GetFieldType(index);

            var typeMapping = _typeMapper.FindMapping(dataType);

            return typeMapping.ChangeDbType(value);
        }

        private void ModifyOrDeleteAffect(ModelUpdateInfo modelUpdateInfo,
           ResultSetMapping resultSetMapping,
           IAdoDbDataVisitor dataVisitor)
        {
            var value = 0;

            if (resultSetMapping == ResultSetMapping.RowCountAndLastInResultSet ||
                resultSetMapping == ResultSetMapping.RowCountAndNotLastInResultSet) //update or delete
            {
                var hasResult = true;

                if (!dataVisitor.MoveNext())
                {
                    hasResult = false;
                }

                var dataReader = dataVisitor.ReadData();

                if (hasResult)
                {
                    var dbValue = this.GetDbValue(dataReader, 0);

                    value = ValueConvert.Convert<int>(dbValue);
                }
                else if (modelUpdateInfo.OutputParameters?.Count > 0)
                {
                    var outputParameter = modelUpdateInfo.OutputParameters
                        .Where(m => m.Value == false)
                        .FirstOrDefault();

                    if (outputParameter.Key != null)
                    {
                        value = (int)this.GetoutputParameterValue(dataVisitor.GetOutputParameters(), outputParameter.Key.ParameterName);
                    }
                    else
                    {
                        throw new ModelUpdateConcurrencyException("Data changed after selected from database.", modelUpdateInfo);
                    }
                }

                if (resultSetMapping == ResultSetMapping.RowCountAndNotLastInResultSet)
                {
                    if (dataVisitor.NextResult() && value > 0)
                    {
                        this.SetReadValue(modelUpdateInfo, dataVisitor);
                    }
                }
            }

            if (modelUpdateInfo.ModelState == ModelState.Deleted)
            {
                this.DeletedCount += value;
            }
            else if (modelUpdateInfo.ModelState == ModelState.Modified)
            {
                this.ModifiedCount += value;
            }
        }

        private void InsertAffect(ModelUpdateInfo modelUpdateInfo,
            ResultSetMapping resultSetMapping,
            IAdoDbDataVisitor dataVisitor)
        {
            this.InsertedCount++;

            if (resultSetMapping == ResultSetMapping.SelectAndLastInResultSet ||
                resultSetMapping == ResultSetMapping.SelectAndNotLastInResultSet)
            {
                this.SetReadValue(modelUpdateInfo, dataVisitor);
            }
        }

        private object GetoutputParameterValue(IReadOnlyList<IAdoDbParameter> outputParameters, string parameterName)
        {
            var parameter = outputParameters.Where(m => m.ParameterName.Contains(parameterName))
                .FirstOrDefault();

            return parameter?.Value;
        }

        private void SetReadValue(ModelUpdateInfo modelUpdateInfo, IAdoDbDataVisitor dataVisitor)
        {
            var readOperations = modelUpdateInfo.ColumnModifications.Where(o => o.IsRead).ToList();
            var outputParameters = dataVisitor.GetOutputParameters();

            var hasResult = false;

            if (dataVisitor.HasRows)
            {
                dataVisitor.MoveNext();
                hasResult = true;
            }

            if (!hasResult && outputParameters.Count < 1 && readOperations.Count > 0)
            {
                throw new ModelUpdateException("Fail to get identity value from database.", null, modelUpdateInfo);
            }

            var dataReader = dataVisitor.ReadData();

            var index = 0;

            foreach (var readOperation in readOperations)
            {
                object value;

                if (hasResult)
                {
                    value = this.GetDbValue(dataReader, index);

                    index++;
                }
                else
                {
                    value = this.GetoutputParameterValue(outputParameters, readOperation.ParameterName);
                }

                if (value == DBNull.Value)
                {
                    value = null;
                }

                readOperation.UpdatableModel.SetIdentityValue(readOperation.Property, value);
            }
        }
    }
}
